package com.ingenico.piccolo.event;

import java.awt.Adjustable;
import java.awt.Component;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.logging.Logger;

import javax.swing.JScrollPane;

import com.ingenico.insider.util.Constants;

import sun.awt.EventListenerAggregate;
import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PCanvas;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.util.PAffineTransform;
import edu.umd.cs.piccolo.util.PBounds;

public class CameraModificationTracker implements PropertyChangeListener, AdjustmentListener {
	private static final Logger _logger = Logger.getLogger(CameraModificationTracker.class.getCanonicalName()); 
	private PCamera sourceCamera;

	private boolean isChanging;
	private Object  interactionSource;

	private PBounds oldViewBounds;
	private PAffineTransform oldViewTransform;

	// Manages the listener list.
	private transient EventListenerAggregate listeners;

	protected void firePositionChange(CameraModificationEvent e) {
		_logger.finest(Constants.ENTRY);
		final Object oldValue = e.getOldValue();
		final Object newValue = e.getNewValue();

		if (oldValue != null && newValue != null && oldValue.equals(newValue)) {
			return;
		}		
		if (listeners != null) {
			Object[] list = listeners.getListenersInternal();
			_logger.finest("" + list.length + " targets must be notified.");
			for (int i = 0; i < list.length; i++) {
				CameraModificationListener target = (CameraModificationListener)list[i];
				target.positionChange(e);
				_logger.finest("target " + target + " notified.");
			}
		} else {
			_logger.finest("no listeners defined yet...");
		}
	}

	protected void fireScaleChange(CameraModificationEvent e) {
		_logger.finest(Constants.ENTRY);
		final Object oldValue = e.getOldValue();
		final Object newValue = e.getNewValue();

		if (oldValue != null && newValue != null && oldValue.equals(newValue)) {
			return;
		}		
		if (listeners != null) {
			Object[] list = listeners.getListenersInternal();
			_logger.finest("" + list.length + " targets must be notified.");
			for (int i = 0; i < list.length; i++) {
				CameraModificationListener target = (CameraModificationListener)list[i];
				target.scaleChange(e);
				_logger.finest("target " + target + " notified.");
			}
		} else {
			_logger.finest("no listeners defined yet...");
		}
	}

	protected void fireSizeChange(CameraModificationEvent e) {
		_logger.finest(Constants.ENTRY);
		final Object oldValue = e.getOldValue();
		final Object newValue = e.getNewValue();

		if (oldValue != null && newValue != null && oldValue.equals(newValue)) {
			return;
		}		
		if (listeners != null) {
			Object[] list = listeners.getListenersInternal();
			_logger.finest("" + list.length + " targets must be notified.");
			for (int i = 0; i < list.length; i++) {
				CameraModificationListener target = (CameraModificationListener)list[i];
				target.sizeChange(e);
				_logger.finest("target " + target + " notified.");
			}
		} else {
			_logger.finest("no listeners defined yet...");
		}
	}

	public CameraModificationTracker(JScrollPane scrollpane) {
		Component component = scrollpane.getViewport().getView();
		if (component instanceof PCanvas) { 
			final PCanvas canvas = (PCanvas)component;
			sourceCamera = canvas.getCamera();
			sourceCamera.addPropertyChangeListener(this);
			canvas.addPropertyChangeListener(this);
			scrollpane.getHorizontalScrollBar().addAdjustmentListener(this);
// TODO: is this necessary as well ?
//			scrollpane.getVerticalScrollBar().addAdjustmentListener(this);
			this.isChanging = false;
		}
	}

	public synchronized boolean removeGeometryChangeListener(CameraModificationListener listener) {
		if (listener == null) {
			return false;
		}
		if (listeners == null) {
			return false;
		}
		return listeners.remove(listener);
	}

	public synchronized void addGeometryChangeListener(CameraModificationListener listener) {
		if (listener == null) {
			return;
		}
		if (listeners == null) {
			listeners = new EventListenerAggregate(CameraModificationListener.class);
		}
		listeners.add(listener);
	}

	private boolean sizeChanged(PBounds newViewBounds) {
		return ((oldViewBounds == null) || (oldViewBounds.equals(newViewBounds)));
	}

	private boolean positionChanged(PAffineTransform newViewTransform) {
		return ( (oldViewTransform == null) || (oldViewTransform.getTranslateX() != newViewTransform.getTranslateX()));
	}

	private boolean scaleChanged(PAffineTransform newViewTransform) {
		return ((oldViewTransform == null) || (oldViewTransform.getScaleX() != newViewTransform.getScaleX()));
	}

	@Override
	public void propertyChange(PropertyChangeEvent e) {
		final Object eventSource = e.getSource();

		if (eventSource instanceof PCanvas) {
			_logger.finest("PCanvas event found");
			final String propertyName = e.getPropertyName();

			// PCanvas notifies us when user interaction starts and stop
			if ( PCanvas.PROPERTY_INTERACTING.equals(propertyName) ) {
				
				// once an interaction has started, we lock on to the originating source 
				if ( (interactionSource == null) || (interactionSource == eventSource) ){
					// We store the interaction state
					isChanging = (Boolean)e.getNewValue();
					if (isChanging) {
						interactionSource = eventSource;
					} else {
						interactionSource = null;

						// If we are not interacting anymore, then we may need to notify listeners
						final PAffineTransform newViewTransform = sourceCamera.getViewTransform();
						final CameraModificationEvent gce = new CameraModificationEvent(
								sourceCamera,
								oldViewTransform,
								newViewTransform
						);

						// We do not want to fire both events here.
						// Scale change is the most destructive event and implies that the bound and position changed as well...
						if (scaleChanged(sourceCamera.getViewTransform())) {
							fireScaleChange(gce);
						} else if (positionChanged(sourceCamera.getViewTransform())) {
							firePositionChange(gce);
						}

						// Update internal state
						oldViewBounds    = sourceCamera.getViewBounds();
						oldViewTransform = newViewTransform;
					}

				}
			}
		}
		if (eventSource instanceof PCamera) {
			final PCamera sourceCamera = (PCamera)eventSource;
			final String propertyName = e.getPropertyName();

			if (PNode.PROPERTY_BOUNDS.equals(propertyName)) {
				final PBounds newViewBounds = sourceCamera.getViewBounds();
				final CameraModificationEvent gce = new CameraModificationEvent(
						sourceCamera,
						oldViewBounds,
						newViewBounds
				);
				fireSizeChange(gce);
				oldViewBounds    = newViewBounds;
				oldViewTransform = sourceCamera.getViewTransform();
			}
		}
	}

	public void adjustmentValueChanged(AdjustmentEvent evt) {
		Adjustable source = evt.getAdjustable();

		// once an interaction has started, we lock on to the originating source 
		if ( (interactionSource == null) || (interactionSource == source) ) {
			isChanging = evt.getValueIsAdjusting();
			if (isChanging) {
				interactionSource = source;
			} else {
				interactionSource = null;

				// If we are not interacting anymore, then we may need to notify listeners
				final PAffineTransform newViewTransform = sourceCamera.getViewTransform();
				final CameraModificationEvent gce = new CameraModificationEvent(
						sourceCamera,
						oldViewTransform,
						newViewTransform
				);

				// We do not want to fire both events here.
				// Scale change is the most destructive event and implies that the bound and position changed as well...
				if (scaleChanged(sourceCamera.getViewTransform())) {
					fireScaleChange(gce);
				} else if (positionChanged(sourceCamera.getViewTransform())) {
					firePositionChange(gce);
				}

				// Update internal state
				oldViewBounds    = sourceCamera.getViewBounds();
				oldViewTransform = sourceCamera.getViewTransform();
			}
		}
	}

}
